package cz.zcu.fav.rat.dataaccess.service.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;

import cz.zcu.fav.rat.beans.Period;
import cz.zcu.fav.rat.beans.PickAction;
import cz.zcu.fav.rat.beans.Reservation;
import cz.zcu.fav.rat.beans.ReturnAction;
import cz.zcu.fav.rat.beans.User;
import cz.zcu.fav.rat.dataaccess.dao.PeriodDAO;
import cz.zcu.fav.rat.dataaccess.service.SettingsFactory;
import cz.zcu.fav.rat.dataaccess.service.PeriodService;
import cz.zcu.fav.rat.rest.utils.RestUtils;

@Service("periodService")
@Transactional
public class PeriodServiceImpl extends
		GenericDataServiceImpl<Period, PeriodDAO> implements PeriodService
{
	@Autowired
	public PeriodServiceImpl(PeriodDAO dao)
	{
		super(dao);
	}

	@Autowired
	SettingsFactory settingsFactory;

	@Override
	public Integer create(Period newInstance)
	{
		if (newInstance.getReservation().getObject().getObjectClass()
				.isPickable() == false)
		{
			newInstance.setPickAction(PickAction.NOT_PICKABLE);
		}
		if (newInstance.getReservation().getObject().getObjectClass()
				.isReturnable() == false)
		{
			newInstance.setReturnAction(ReturnAction.NOT_RETURNABLE);
		}
		return super.create(newInstance);
	}

	@Override
	@Transactional(readOnly = true)
	public List<Period> getPeriodsByUserId(Integer userId)
	{
		return dao.findAllPeriodsByUserId(userId);
	}

	@Transactional(readOnly = true)
	public List<Period> getAllPeriodsByUserIdInDateRange(Integer userId,
			LocalDateTime from, LocalDateTime to)
	{
		// This must be kept in this way as hibernate does not support named
		// descriptors in proper order :(
		return dao.findAllPeriodsByUserIdInDateRange(userId, from, from, from,
				to);
	}

	@Transactional(readOnly = true)
	public List<Period> getAllPeriodsByObjectIdInDateRange(Integer objectId,
			LocalDateTime from, LocalDateTime to)
	{
		// This must be kept in this way as hibernate does not support named
		// descriptors in proper order :(
		return dao.findAllPeriodsByObjectIdInDateRange(objectId, from, from,
				from, to);
	}

	@Override
	@Transactional(readOnly = true)
	public Multimap<Reservation, Period> getValidReservationsByObjectIdInDateRange(
			Integer objectId, LocalDateTime from, LocalDateTime to)
	{
		List<Period> periods = getValidPeriodsByObjectIdInDateRange(objectId,
				from, to);

		Multimap<Reservation, Period> multiMap = ArrayListMultimap.create();

		for (Period period : periods)
		{
			Reservation res = period.getReservation();
			multiMap.put(res, period);
		}
		return multiMap;
	}

	@Override
	@Transactional(readOnly = true)
	public Multimap<Reservation, Period> getPeriodsByUserIdNotReturned(
			int userId)
	{

		Multimap<Reservation, Period> multiMap = ArrayListMultimap.create();

		List<Period> periods = dao.findAllPeriodsByUserIdNonReturned(userId);

		for (Period period : periods)
		{
			multiMap.put(period.getReservation(), period);

		}
		return multiMap;
	}

	public Map<LocalDate, Collection<LocalDate>> getOccupiedDaysByObject(
			Integer objectId, LocalDate since, LocalDate to)
	{
		LocalDateTime sinceAsDateTime = since.toLocalDateTime(new LocalTime(0,
				0, 0));
		LocalDateTime toAsDateTime = to.toLocalDateTime(new LocalTime(23, 59,
				59));

		List<Period> periods = getAllPeriodsByObjectIdInDateRange(objectId,
				sinceAsDateTime, toAsDateTime);

		return getDates(since, to, sinceAsDateTime, toAsDateTime, periods);
	}

	public Map<LocalDate, Collection<LocalDate>> getOccupiedDaysByUser(
			Integer userId, LocalDate since, LocalDate to)
	{
		LocalDateTime sinceAsDateTime = since.toLocalDateTime(new LocalTime(0,
				0, 0));
		LocalDateTime toAsDateTime = to.toLocalDateTime(new LocalTime(23, 59,
				59));

		List<Period> periods = getAllPeriodsByUserIdInDateRange(userId,
				sinceAsDateTime, toAsDateTime);

		return getDates(since, to, sinceAsDateTime, toAsDateTime, periods);
	}

	protected Map<LocalDate, Collection<LocalDate>> getDates(LocalDate since,
			LocalDate to, LocalDateTime sinceAsDateTime,
			LocalDateTime toAsDateTime, List<Period> periods)
	{
		// List<LocalDate> dates = new ArrayList<LocalDate>();
		Multimap<LocalDate, LocalDate> dates = ArrayListMultimap.create();

		for (Period period : periods)
		{
			getDaysInInterval(
					period.getFromDate().isBefore(sinceAsDateTime) ? since
							: new LocalDate(period.getFromDate()), period
							.getToDate().isAfter(toAsDateTime) ? to
							: new LocalDate(period.getToDate()), dates);
		}

		// get rid of not-unique days
		return dates.asMap();
		// dates = ImmutableSet.copyOf(dates).asList();

		// return dates;
	}

	@Override
	@Transactional(readOnly = false)
	public Multimap<Reservation, Period> returnPeriod(int userId, int periodId)
	{

		Period period = get(periodId);
		if (period.getReservation().getUser().getId() != userId)
		{
			return null;
		}
		period.setReturnAction(ReturnAction.RETURNED);
		if (period.getToDate().isAfter(LocalDateTime.now()))
		{
			period.setToDate(LocalDateTime.now());
		}

		update(period);

		Multimap<Reservation, Period> multiMap = ArrayListMultimap.create();
		multiMap.put(period.getReservation(), period);
		return multiMap;
	}

	/**
	 * podiva se jestli je pobliz nejaka perioda ktera se da vzit. Pokud ano,
	 * nastavi ji na Picked a vrati ji. Pokud nas blokuje jina rezervace, vrati
	 * periodu blokujici rezarvace. Pokud neexistuje nic co bychom si mohli
	 * vzit, vrati null
	 * 
	 * @param objectId
	 * @param user
	 * @return
	 */
	@Transactional(readOnly = false)
	public Period pickUp(Integer objectId, User user)
	{
		LocalDateTime now = LocalDateTime.now();
		LocalDateTime to = now.plus(settingsFactory
				.getMinimumReservationDuration());
		Multimap<Reservation, Period> multimap = getValidReservationsByObjectIdInDateRange(
				objectId, now, to);
		Map<Reservation, Collection<Period>> resMap = multimap.asMap();
		Period myPeriodAround = null;

		for (Entry<Reservation, Collection<Period>> entry : resMap.entrySet())
		{
			if (entry.getKey().getUser().equals(user))
			{

				// Do we want to take this one?
				Collection<Period> periodsInDateRange = entry.getValue();
				if (periodsInDateRange.size() != 1)
				{

					throw new RuntimeException(
							"Strange, why is there more than one period??");
				}
				myPeriodAround = periodsInDateRange.iterator().next();
				break;
			}
		}

		if (myPeriodAround != null)
		{
			if (myPeriodAround.getPickAction() == PickAction.NOT_PICKED)
			{
				// Change to picked and take this one in case when others
				// reservations around are
				// finished or not picked at all

				Period alienActivePeriod = null;
				for (Collection<Period> periods : resMap.values())
				{
					for (Period periodInt : periods)
					{
						if (periodInt.getReservation().getUser().equals(user))
						{
							continue;
						}
						alienActivePeriod = periodInt;
						break;
					}
				}

				// TODO: co s alianActivePeriod? cizi aktivni perioda ve stejnem
				// case
				if (alienActivePeriod == null)
				{
					if (myPeriodAround.getPickAction() == PickAction.PICKED)
					{
						// TODO: perioda už je potvrzena, netreba ji tejkovat
						// znova
					}

					myPeriodAround.setPickAction(PickAction.PICKED);
					myPeriodAround.setFromDate(LocalDateTime.now());
					update(myPeriodAround);

					return myPeriodAround;
				} else
				{
					return alienActivePeriod;
				}
			}
		}
		return null;
	}

	@Override
	@Transactional(readOnly = false)
	public Period returnItBack(Period period)
	{
		if (period.getReturnAction() == ReturnAction.NOT_RETURNED
				&& (period.getPickAction() == PickAction.PICKED || period
						.getPickAction() == PickAction.NOT_PICKABLE))

		{
			period.setReturnAction(ReturnAction.RETURNED);
			period.setToDate(period.getToDate().isBefore(LocalDateTime.now()) ? LocalDateTime
					.now() : period.getToDate());
			update(period);
		}
		return period;
	}

	@Override
	@Transactional(readOnly = true)
	public List<Period> getValidPeriodsByUserIdInDateRange(Integer userId,
			LocalDateTime from, LocalDateTime to)
	{
		return dao.findValidPeriodsByUserIdInDateRange(userId, from, from,
				from, to);
	}

	@Override
	@Transactional(readOnly = true)
	public List<Period> getValidPeriodsByObjectIdInDateRange(Integer objectId,
			LocalDateTime from, LocalDateTime to)
	{
		return dao.findValidPeriodsByObjectIdInDateRange(objectId, from, from,
				from, to);
	}

	@Override
	@Transactional(readOnly = false)
	public Period setValid(Period period, boolean valid)
	{
		if (valid)
		{
			// we need to check if there is no other valid period
			Integer objId = period.getReservation().getObject().getId();
			List<Period> periods = getValidPeriodsByObjectIdInDateRange(objId,
					period.getFromDate(), period.getToDate());
			if (!periods.isEmpty())
			{
				// return periods.get(0);
				// we cant switch periods state
				return period;
			}
		}

		period.setValid(valid);
		period = merge(period);
		update(period);
		return period;
	}

	public static void getDaysInInterval(LocalDate start, LocalDate end,
			Multimap<LocalDate, LocalDate> days)
	{
		for (LocalDate day = start; day.isBefore(end); day = day.plusDays(1))
		{
			days.put(day, day);
		}
		days.put(end, end);
	}

}
